package entity;

import static org.lwjgl.input.Keyboard.*;
import static org.lwjgl.input.Mouse.getDX;
import static org.lwjgl.input.Mouse.getDY;
import static org.lwjgl.opengl.GL11.*;
import level.Level;

import org.lwjgl.util.vector.Matrix4f;
import org.lwjgl.util.vector.Vector3f;

import entity.characters.Player;
import shader.Filter;
import game.util.MatrixUtil;

/**
 * @author Jevgeni Krutov
 * @author Sri Harsha Chilakapati
 */
public final class Camera extends MovingEntity {
	
	public static final float DEF_FOV = 70f;
	public static final float DEF_Z_NEAR = 0.1f;
	public static final float DEF_Z_FAR = 100f;
	
	public Filter filter;
	
	private float fov;
	private float aspect;
	private float zNear;
	private float zFar;

	private Matrix4f projection;
	private Matrix4f view;
	
	private int observedW = 0;

	private boolean isFreeFloatingMode = false;
	
	private Matrix4f viewModel;
	private Player host;
	private Level level;

	/**
	 * Creates camera with default fov, z-near and z-far
	 */
	public Camera(Vector3f position, Vector3f rotation, float aspect, Level level) {
		this(position, rotation, DEF_FOV, aspect, DEF_Z_NEAR, DEF_Z_FAR, level);
	}
	
	/**
	 * Creates a simple 3D Perspective Camera.
	 * 
	 * @param fov
	 *            The field of view in degrees.
	 * @param aspect
	 *            The aspect ratio.
	 * @param zNear
	 *            The near clipping plane.
	 * @param zFar
	 *            The far clipping plane.
	 */
	public Camera(Vector3f position, Vector3f rotation, float fov, float aspect, float zNear, float zFar, Level level) {
		super(position, rotation);
		
		this.fov = fov;
		this.aspect = aspect;
		this.zNear = zNear;
		this.zFar = zFar;
		this.level = level;

		projection = MatrixUtil.createPerspectiveProjection(fov, aspect, zNear, zFar);
		view = MatrixUtil.createIdentityMatrix();
		viewModel = new Matrix4f();

		position = new Vector3f(0, 0, 0);
		rotation = new Vector3f(0, 0, 0);
		
		filter = new Filter();

		// Enable depth testing
		glEnable(GL_DEPTH_TEST);
	}

	/**
	 * Apply the camera's transformations.
	 */
	public void apply() {
		Matrix4f.invert(getMatrix(), view);
	}
	
	/**
	 * Camera behaves as free-floating in response to user input
	 */
	@Override
	public void listenToActionAndMove(long elapsedTime) { 
		if (isKeyDown(KEY_2))
			setFreeFloatingMode(true);
		if (isKeyDown(KEY_1))
			setFreeFloatingMode(false);
		
		
		lookAtCurrentW();
		
		if(host.isAllowedToChangeW()) {
			if (isKeyDown(KEY_Q)) {
				if(level.getCollisionDetector().checkAndSetAllowedAnaForPlayer(host, level.entities)) {
					lookAna();
				} else {
					filter.setGray();
				}
			}
			if (isKeyDown(KEY_E)) {
				if(level.getCollisionDetector().checkAndSetAllowedKataForPlayer(host, level.entities)) {
					lookKata();
				} else {
					filter.setGray();
				}
			}
		}
		
		
		if(isFreeFloatingMode) {
	        // Look up/down
	        addRotation(getDY() * 0.5f, 0, 0);
	        // Turn left/right
	        addRotation(0, -getDX() * 0.5f, 0);

	        // Move front
	        if (isKeyDown(KEY_W))
	            moveInDirection(-0.1f, 0);

	        // Move back
	        if (isKeyDown(KEY_S))
	            moveInDirection(0.1f, 0);

	        // Strafe left
	        if (isKeyDown(KEY_A))
	            moveInDirection(0.1f, -1);

	        // Strafe right
	        if (isKeyDown(KEY_D))
	            moveInDirection(-0.1f, -1);
	        
	        // Altitude up
	        if (isKeyDown(KEY_SPACE))
	        	position.y += 0.1f;
	        
	        // Altitude down
	        if (isKeyDown(KEY_LCONTROL))
	        	position.y -= 0.1f;
		} else {
			copyHostPositionAndRotation();
		}
	}
	
	private void copyHostPositionAndRotation() {
		position.set(host.getPosition());
		rotation.set(host.getRotation());
		invalidateMatrix();
	}
	
	/**
	 * calculates and returns normal matrix for this camera and the model given
	 */
    public Matrix4f getNormalMatrix(Matrix4f modelMatrix) {   	
		Matrix4f mNormal = new Matrix4f();
		Matrix4f.mul(getViewMatrix(), modelMatrix, mNormal);
	    mNormal.m30 = 0;
	    mNormal.m31 = 0;
	    mNormal.m32 = 0;
	    mNormal.m33 = 1;
	    Matrix4f.invert(mNormal, mNormal);
	    Matrix4f.transpose(mNormal, mNormal);
	    return mNormal;
    }
    
	@Override
	public void setSingleW(int w) {
		super.setSingleW(w);
		filter.startTransition();
		lookAtCurrentW();
	}
    
    // methods to observe different W-s
    
	public void lookAna() {
		if(observedW != level.getMaxW()) {
			filter.setGreen();
			observedW++;
		} else {
			filter.setGray();
			System.out.println("nothing to show ana");
		}
	}

	public void lookKata() {
		if(observedW != level.getMinW()) {
			filter.setRed();
			observedW--;
		} else {
			filter.setGray();
			System.out.println("nothing to show kata");
		}
	}

	public void lookAtCurrentW() {
		filter.reset();
		observedW = host.getCurrentW();
	}
	
	public void setHostAndSetThisAsHisCamera(Player player) {
		this.host = player;
		player.setCamera(this);
	}
    
    
    // basic getters and setters below
    
	public float getFieldOfView() {
		return fov;
	}

	public float getAspectRatio() {
		return aspect;
	}

	public float getNearPlane() {
		return zNear;
	}

	public float getFarPlane() {
		return zFar;
	}

	public Matrix4f getProjectionMatrix() {
		return projection;
	}

	public Matrix4f getViewMatrix() {
		return view;
	}

	public Matrix4f getViewModel(Matrix4f modelMatrix) {
		Matrix4f.mul(view, modelMatrix, viewModel);
		return viewModel;
	}
	
	public boolean isFreeFloatingMode() {
		return isFreeFloatingMode;
	}
	
	public void setFreeFloatingMode(boolean isFreeFloatingMode) {
		this.isFreeFloatingMode = isFreeFloatingMode;
	}
	
	public Entity getHost() {
		return host;
	}
	
	public int getObservedW() {
		return observedW;
	}
}